Upgrade supply chain demo to scoped capabilities with active defense - #76
Conversation
…fense Replace custom kernel/rootfs build with pyhl for the Hyperlight sandbox. The guest now has real capabilities (--mount for workspace, --net-allow for example.com) and performs legitimate work (read input, fetch HTTP, write output) while every malicious action is still actively blocked by policy enforcement: loopback denied, link-local denied, scoped mount, no env forwarding. Add cloud metadata probing phase (IMDS) to stealer. Update graphic and README to reflect active defense model. Signed-off-by: danbugs <danilochiarlone@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the supply-chain demo to run via pyhl (warmed Python snapshot) instead of a custom Unikraft kernel/rootfs build, and expands the “malicious” package behavior to include cloud metadata probing while demonstrating Hyperlight policy enforcement with scoped mount/network capabilities.
Changes:
- Replace the
hl-unikraft/demo workflow (Dockerfile +kraft.yaml) with apyhl-drivenJustfile(just setup,just run,just run-minimal). - Extend the simulated stealer to probe IMDS endpoints (AWS/Azure/GCP) and improve “blocked” status messaging.
- Update demo materials (README, new graphic, victim app) to include legitimate work (read input, fetch example.com, write output).
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| demos/supply-chain/victim_app.py | Adds workspace IO + allowed network fetch to represent legitimate workload. |
| demos/supply-chain/reqeusts/stealer.py | Adds IMDS probing phase and updates summary output. |
| demos/supply-chain/README.md | Updates narrative and run instructions for the new pyhl workflow. |
| demos/supply-chain/hl-unikraft/Justfile | Replaces build pipeline with pyhl setup/run commands and demo scaffolding. |
| demos/supply-chain/hl-unikraft/kraft.yaml | Removed (no longer needed with pyhl). |
| demos/supply-chain/hl-unikraft/Dockerfile | Removed (no longer needed with pyhl). |
| demos/supply-chain/graphic.html | Adds an updated graphic illustrating scoped capabilities + active defense. |
| demos/supply-chain/bare-metal/run.sh | Adds workspace input/output handling for legitimate workload on bare metal. |
| demos/supply-chain/.gitignore | Ignores generated demo artifacts under hl-unikraft/. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Install `pyhl` (the Hyperlight Python runtime): | ||
|
|
||
| ```bash | ||
| cargo install --path tools/pyhl |
| # One-time setup — installs kernel + rootfs, warms Python snapshot | ||
| just setup | ||
|
|
||
| # Scoped sandbox: mount + network, attack still contained | ||
| just run | ||
|
|
||
| # Run | ||
| just run # Execute inside the sandbox | ||
| # Minimal sandbox: no mount, no network (total isolation) | ||
| just run-minimal |
| for workspace access, `--net-allow example.com` for network). Legitimate work | ||
| succeeds, but every malicious action is blocked by active policy enforcement: | ||
|
|
||
| - **Credential theft → BLOCKED** — `~/.ssh/id_rsa`, `~/.aws/credentials`, etc. | ||
| are outside the mounted workspace. The guest has filesystem access, but only |
| # Install pyhl image with hostfs + networking baked into snapshot | ||
| setup: prepare | ||
| pyhl setup --mount {{guest_dir}} --net | ||
|
|
||
| # Scoped sandbox: guest has mount + network, but attack is still contained | ||
| # - Legitimate work succeeds: reads input.txt, GETs example.com, writes output.txt | ||
| # - Credential theft: BLOCKED (files outside mount don't exist) | ||
| # - Environment variables: NOT SET (host env never forwarded) | ||
| # - C2 exfiltration: BLOCKED (loopback always denied) | ||
| # - Cloud metadata: BLOCKED (link-local always denied) | ||
| # - Persistence: BLOCKED (host dotfiles not mounted) | ||
| run: prepare | ||
| pyhl run \ | ||
| --mount {{guest_dir}} \ | ||
| --net-allow example.com \ | ||
| -c "import sys; sys.path.insert(0, '/host'); exec(open('/host/victim_app.py').read())" |
| <div class="row"><span class="key">read /host/input.txt</span> <span class="ok">OK</span></div> | ||
| <div class="row"><span class="key">GET example.com</span> <span class="ok">OK (HTTP 200)</span></div> | ||
| <div class="row"><span class="key">write /host/output.txt</span> <span class="ok">OK</span></div> |
| # ── Summary ────────────────────────────────────────────────────── | ||
| n_files = len(stolen_files) | ||
| n_env = len(stolen_env) | ||
| n_meta = len(stolen_metadata) | ||
|
|
||
| print() | ||
| print("=" * (W + 2)) | ||
| if n_files > 0 or n_env > 0: | ||
| print(" RESULT: Attack SUCCEEDED") | ||
| print(f" {n_files} credential file(s) stolen") | ||
| print(f" {n_env} environment variable(s) harvested") | ||
| print(f" {n_meta} cloud metadata endpoint(s) reached") | ||
| print(f" {persistence_count} persistence mechanism(s) installed") |
| except Exception as e: | ||
| msg = str(e) | ||
| if "link-local" in msg.lower(): | ||
| print(f"{_dots(name)} BLOCKED (link-local policy)") | ||
| elif "Input/output error" in msg: | ||
| print(f"{_dots(name)} BLOCKED (link-local policy)") | ||
| elif "timed out" in msg or "timeout" in msg.lower(): | ||
| print(f"{_dots(name)} TIMEOUT (no metadata service)") | ||
| elif "refused" in msg.lower(): | ||
| print(f"{_dots(name)} NOT AVAILABLE") | ||
| else: | ||
| print(f"{_dots(name)} BLOCKED") |
| A simple application that fetches data from an API. | ||
|
|
||
| The developer installed `reqeusts` instead of `requests` — a common | ||
| The developer installed `reqeusts` instead of `requests` --- a common |
|
|
||
| <!-- HYPERLIGHT SANDBOX --> | ||
| <div class="col col-pass"> | ||
| <div class="col-title">Hyperlight Micro-VM <span class="cap">(--mount ./workspace --net-allow example.com)</span></div> |
There was a problem hiding this comment.
Linux Benchmarks
Details
| Benchmark suite | Current: e1ff469 | Previous: d8524a9 | Ratio |
|---|---|---|---|
hello_world (median) |
20 ms |
20 ms |
1 |
pandas (median) |
110 ms |
110 ms |
1 |
density (per VM) |
7 MB |
7 MB |
1 |
snapshot (disk) |
385 MiB |
385 MiB |
1 |
This comment was automatically generated by workflow using github-action-benchmark.
There was a problem hiding this comment.
Windows Benchmarks
Details
| Benchmark suite | Current: e1ff469 | Previous: d8524a9 | Ratio |
|---|---|---|---|
hello_world (median) |
298 ms |
179 ms |
1.66 |
pandas (median) |
949 ms |
508 ms |
1.87 |
density (per VM) |
6 MB |
6 MB |
1 |
snapshot (disk) |
392 MiB |
392 MiB |
1 |
This comment was automatically generated by workflow using github-action-benchmark.
- Fix pyhl install path in README (host crate, not tools/) - Fix run-minimal description (mount only, not total isolation) - Fix README wording (mounted directory, not workspace) - Fix graphic: correct mount flag, correct workspace paths - Add metadata to attack success condition - Use structured errno checks instead of string matching - Fix docstring dash Signed-off-by: danbugs <danilochiarlone@gmail.com>
Summary
pyhl— no build step needed, justjust setup+just run--mount ./workspace,--net-allow example.com) and performs legitimate work: reads input file, fetches from example.com, writes outputTest plan
cd demos/supply-chain && ./bare-metal/run.sh— attack succeeds, legitimate work succeedscd demos/supply-chain/hl-unikraft && just setup && just run— legitimate work succeeds, attack containedcd demos/supply-chain/hl-unikraft && just run-minimal— network fetch fails, everything else same as scoped